package com.gameislive.browser;

import java.util.*;

/**
 * this class represents xml tags. they should 
 * have a name and a state, and a hash of parms
 * if applicable. (currently a string...)
 *
 * waplet classes are not currently in a package, because 
 * of strange browser/jar behavior with packages.
 * 
 * @author kxml
 */
public class Tag{

    /** tag state for an undefined tag */
    public static final int UNDEFINED = 0;

    /** tag state for an open tag */
    public static final int TAG_OPEN = 1;

    /** tag state for a close tag (starts with /) */
    public static final int TAG_CLOSE = 2;
    
    /** tag state for a self-contained tag (ends with /) */
    public static final int TAG_SELFCONTAINED = 3;
	
    /** stores the text tag passed in */
    private String _tagtext;

    /** the tag name, or primary identifier */
    private String _name;

    /** a hash of parameters for this tag */
    private Hashtable _parms;

    /** an internal field representing the tag state */
    private int _state; 

    /** constructor creates an empty tag with undefined state. */
    public Tag(){
    	_name = "";
    	_parms = null;
    	_state = UNDEFINED;
    }

    /** constructor parses a text representation. */
    public Tag(String text){
    	this();
    	_tagtext = text;

    	if(!(text.charAt(0) == '<' && text.charAt(text.length()-1) == '>')){
    	    _name = text;
    	}
    	else{
    	    // can't lowercase it here: if a parameter is a url, it
    	    // needs to maintain case.
    	    // text = text.substring(1, text.length() - 1).toLowerCase();

    	    // clear out opening and closing brackets
    	    text = text.substring(1, text.length() - 1);

    	    // check the state
    	    if(text.charAt(0) == '/'){
        		text = text.substring(1).trim();
        		_state = TAG_CLOSE;
    	    }
    	    else if(text.charAt(text.length()-1) == '/'){
        		text = text.substring(0, text.length() - 1).trim();
        		_state = TAG_SELFCONTAINED;
    	    }
    	    else{
        		text = text.trim();
        		_state = TAG_OPEN;
    	    }

    	    // check for parameters, if any: parse them.
    	    int idx = 0;
    	    if((idx = text.indexOf(' ')) > -1){
        		parseParms( text.substring(idx + 1).trim());
        		// now that you have the parms, lower case the id...
        		text = text.substring(0, idx).trim().toLowerCase();
    	    }
    	    _name = text;
    	}
    }

    /** 
     * the string is chopped into name value pairs, and pairs 
     * are stored in the parameter hash. watch out for quoted values:
     * don't put the quotes in the hash. there shouldn't be
     * any space between the key and the value, each pair should
     * look like key=value.
     */
    public void parseParms( String s){
    	
    	_parms = new Hashtable();
    	String key = null;
    	String val = null;
    	int idx = 0;
    	int eidx = 0;
    	
    	while((idx = s.indexOf('=')) > -1){
    	    key = s.substring(0, idx).trim();
    	    s = s.substring(idx + 1).trim();
    	    // is s quoted? if so, look for the end quote...
    	    if(s.charAt(0) == '"')
    	    	val = s.substring(1, (eidx = s.indexOf('"', 1)));	    
    	    else {
        		eidx = s.indexOf( ' ');
        		if( eidx == -1) eidx = s.length() - 1;
        		val = s.substring(0, eidx);
    	    }
    	    s = s.substring(eidx + 1).trim();
    	    _parms.put(key, val);
    	}
    }

    /** accessor method for tag id */
    public String getName(){ return _name; }

    /** set the id of a tag */
    public void setName(String name){ this._name = name; }

    /** accessor method for parameter hash */
    public Hashtable getParms(){ return _parms; }

    /** set the parameter hash directly */
    public void setParms(Hashtable parms){ this._parms = parms; }

    /** accessor method for a single parameter value, by key (name). */
    public String getParm(String key){
    	Object obj = null;
    	if(null == _parms) return null;
    	return (null == (obj = _parms.get(key))) ? null : (String)obj;
    }

    /** 
     * set a single parameter name/value pair. this method will 
     * overwrite existing values.
     */
    public void setParm(String key, String val){
    	if(null == _parms) _parms = new Hashtable();
    	_parms.put(key, val);
    }

    /** accessor method for the tag state. */
    public int getState(){ return _state; }

    /** set the tag state directly. */
    public void setState(int state){ this._state = state; }

    /** 
     * return a string representation of the tag. this includes the
     * name and all name-value parameter pairs. (this is a descriptor
     * method, it won't return the actual tag that was passed in). 
     */
    public String toString(){
    	StringBuffer sb = 
    	    new StringBuffer("Tag name=<" + _name + "> state = " + _state);
    	if(null != _parms){
    	    String key = null;
    	    for(Enumeration e = _parms.keys(); e.hasMoreElements(); ){
        		key = ((String)(e.nextElement()));
        		sb.append("\n\t" + key + "=[" + 
                                  ((String)(_parms.get(key))) + "]");
    	    }
    	}
    	return sb.toString();
    }
    
    /**
     * this method return a tag,if parm is undefined,then return null
     */
    public static Tag MakeTag(Object o){
    	Tag t = new Tag((String)o);
    	return (t.getState() == Tag.UNDEFINED) ? null : t;
    }

    /** 
     * this method builds the document tree. it'll throw an
     * exception on a document error, which is handy for 
     * validating page code. it should probably display a 
     * specific error screen, but for now it just dumps to 
     * std out.
     *
     * store the tree: put it all in a vector. don't worry
     * about level indices, because it'll fail here if it's bad.
     * start text with a ' (&apos;).
     *
     * catch exceptions, for a better stack trace, but then
     * throw them back.
     */
    public static Vector Tree( Vector v) throws Exception{
    	// maintain a hash, keyed by 'level', with values
    	// of the tag names. fail on a bad match.

    	Hashtable tree = new Hashtable();
    	Vector document = new Vector();

    	// temporary tag, string ref, object holder
    	Tag tag = null;
    	String tmp = null;
    	Object obj = null;

    	// current level
    	int currentLevel = -1;

    	boolean ignore = false;
    	
    	for(Enumeration e = v.elements(); e.hasMoreElements(); ){
    	    tmp = ( null == ( obj = e.nextElement())) ? "" : (String)obj;
    	    
    	    //if( null == obj) System.out.println("null element!");
    	    //System.out.println("el: " + tmp);
    	    if(tmp.startsWith("<!--")){
    	    	ignore = true;
    	    }
    	    
    	    if(ignore){
    	    	if(tmp.endsWith("-->")){
    	    		ignore = false;
    	    	}
    	    	System.out.println("IGNORE: "+tmp);
    	    	continue;
    	    }
    	    
    	    if(null != (tag = MakeTag(tmp))){
        		if(tag.getState() == Tag.TAG_OPEN){
        		    currentLevel++;
        		    tree.put(new Integer(currentLevel), tag);
        		    document.addElement(tag);
        		}
        		else if(tag.getState() == Tag.TAG_CLOSE){
        		    if( null == tree.get( new Integer( currentLevel))){
            			String xmessage = 
            			    " attempted close of </" + tag.getName() +
            			    "> at root level";
            			throw new Exception( xmessage);
        		    }
        		    else if(tag.getName().equals(((Tag)(tree.get(
        		        new Integer(currentLevel)))).getName())){
            			currentLevel--;
            			document.addElement(tag);
        		    }
        		    else{
            			String xmessage = 
            			    " attempted close of </" + tag.getName() +
            			    "> in body of <" + ((Tag)(tree.get(
                                        new Integer(currentLevel)))).getName() + ">";
            			throw new Exception( xmessage);
        		    }		    
        		}
        		else if(tag.getState() == Tag.TAG_SELFCONTAINED){
        		    document.addElement(tag);
        		}
    	    }
    	    // it's text...
    	    else document.addElement(new String(CleanString(tmp)));
    	}
    	return document;
    }
    
    /** 
     * this method handles escaped chars in the string, and
     * also the double $ substitutions. the renderer just takes
     * plain text.
     */
    private static String CleanString(String s){
    	// first, translate $ to $...
    	int idx = 0;
    	while((idx = s.indexOf( "$")) > -1){
    	    s = s.substring(0, idx) + s.substring(idx+1);
    	}

    	StringBuffer sb = new StringBuffer("");
    	StringBuffer character = new StringBuffer("");

    	int len = s.length();
    	idx = 0;
    	char c = ' ';

    	boolean esc = false;
    	
    	// loop keys on &amp; and ; to parse escaped characters.
    	// it then does a lookup to swap the strings.
    	while(idx < len){
    	    c = s.charAt(idx);
    	    if( esc){
        		character.append(c);
        		if( c == ';'){
        			String tmp = character.toString();
        			if(tmp.startsWith("&#x")){
        				tmp = Tools.Convert(tmp);				
        			}
        			tmp = Tools.ReplaceTagString(tmp);
        			sb.append(tmp);
        			//obj = escapeCharacters.get( tmp);
        		    //if( null != obj) sb.append((String)obj);		    		    
        		    esc = false;
        		}
    	    }
    	    else if( c == '&'){
        		character = new StringBuffer();
        		character.append(c);
        		esc = true;
    	    }	    
    	    else{
    	    	sb.append(c);
    	    }
    	    idx++;
    	} 

    	return sb.toString();
    }
}
